/*
 * Copyright (C) 2012 Google Inc.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package interactivespaces.hardware.driver.gaming.wii;

import interactivespaces.InteractiveSpacesException;
import interactivespaces.hardware.driver.DriverSupport;
import interactivespaces.service.comm.serial.bluetooth.BluetoothCommunicationEndpoint;
import interactivespaces.service.comm.serial.bluetooth.BluetoothCommunicationEndpointService;

import java.util.List;
import java.util.concurrent.Future;

import com.google.common.collect.Lists;

/**
 * A driver for the Wii remote.
 * 
 * @author Keith M. Hughes
 */
public class WiiRemoteDriver extends DriverSupport {

	/**
	 * The Bluetooth report for reading from the Wii remote.
	 */
	public static final int WII_REMOTE_RECEIVE_PORT = 13;

	/**
	 * The Bluetooth report for writing to the Wii remote.
	 */
	public static final int WII_REMOTE_SEND_PORT = 11;

	/**
	 * Command for setting the lights.
	 */
	private static final byte COMMAND_LIGHT = 0x11;

	private static final byte[] FULL_COMMAND_LIGHT_0 = { 0x52, COMMAND_LIGHT,
			0x10 };
	private static final byte[] FULL_COMMAND_LIGHT_1 = { 0x52, COMMAND_LIGHT,
			0x20 };
	private static final byte[] FULL_COMMAND_LIGHT_2 = { 0x52, COMMAND_LIGHT,
			0x40 };
	private static final byte[] FULL_COMMAND_LIGHT_3 = { 0x52, COMMAND_LIGHT,
			(byte) 0x80 };

	private static final byte[][] FULL_COMMAND_LIGHTS = { FULL_COMMAND_LIGHT_0,
			FULL_COMMAND_LIGHT_1, FULL_COMMAND_LIGHT_2, FULL_COMMAND_LIGHT_3 };

	/**
	 * The command for telling the remote to calibrate.
	 */
	private static byte COMMAND_READ_CALIBRATION = 0x17;

	/**
	 * The full command for reading remote calibration.
	 */
	private static byte[] FULL_COMMAND_READ_CALIBRATION = new byte[] { 0x52,
			COMMAND_READ_CALIBRATION, 0x00, 0x00, 0x00, 0x16, 0x00, 0x08 };

	/**
	 * Command for setting the reporting that the rmeote provides.
	 */
	private static final int COMMAND_SET_REPORT = 0x52;

	/**
	 * The calibration response report.
	 */
	private static final byte REPORT_CALIBRATION_RESPONSE = 0x21;

	/**
	 * A button event only is being reported.
	 */
	private static final byte REPORT_BUTTON_ONLY = 0x30;

	/**
	 * A button and accelerometer event are being reported.
	 */
	private static final byte REPORT_BUTTON_ACCELEROMETER = 0x31;

	/**
	 * The full command for setting the report to button only.
	 */
	private static final byte[] FULL_COMMAND_SET_REPORT_BUTTON = new byte[] {
			COMMAND_SET_REPORT, 0x12, 0x00, REPORT_BUTTON_ONLY };

	/**
	 * The full command for setting the report to button and accelerometer.
	 */
	private static final byte[] FULL_COMMAND_SET_REPORT_BUTTON_ACCELEROMETER = new byte[] {
			COMMAND_SET_REPORT, 0x12, 0x00, REPORT_BUTTON_ACCELEROMETER };

	/**
	 * The Bluetooth address for the remote.
	 */
	private String address;

	/**
	 * The endpoint for speaing with the remote.
	 */
	private BluetoothCommunicationEndpoint remoteEndpoint;

	/**
	 * The reader future.
	 */
	private Future<?> readerFuture;

	/**
	 * Denominator for the X calibration for the accelerometer.
	 */
	private double calibrationX;

	/**
	 * Denominator for the Y calibration for the accelerometer.
	 */
	private double calibrationY;

	/**
	 * Denominator for the Z calibration for the accelerometer.
	 */
	private double calibrationZ;

	/**
	 * 0 point for the X calibration for the accelerometer.
	 */
	private double calibrationX0;

	/**
	 * 0 point for the Y calibration for the accelerometer.
	 */
	private double calibrationY0;

	/**
	 * 0 point for the Z calibration for the accelerometer.
	 */
	private double calibrationZ0;

	/**
	 * The list of all listeners for events.
	 */
	private List<WiiRemoteEventListener> listeners = Lists.newArrayList();

	/**
	 * Construct a driver for the given address.
	 * 
	 * @param address
	 *            the bluetooth address of the Wii Remote
	 */
	public WiiRemoteDriver(String address) {
		this.address = address;
	}

	@Override
	public void startup() {
		BluetoothCommunicationEndpointService connectionEndpointService = spaceEnvironment
				.getServiceRegistry().getRequiredService(
						BluetoothCommunicationEndpointService.SERVICE_NAME);

		try {
			remoteEndpoint = connectionEndpointService.newDualEndpoint(address,
					WII_REMOTE_RECEIVE_PORT, WII_REMOTE_SEND_PORT);
			remoteEndpoint.startup();
		} catch (Exception e) {
			throw new InteractiveSpacesException("Unable to connect to device",
					e);
		}

		readerFuture = spaceEnvironment.getExecutorService().submit(
				new RemoteReader());

		readCalibration();
	}

	@Override
	public void shutdown() {
		if (readerFuture != null) {
			readerFuture.cancel(true);
			readerFuture = null;
		}

		if (remoteEndpoint != null) {
			remoteEndpoint.shutdown();
			remoteEndpoint = null;
		}
	}

	/**
	 * Add a new event listener.
	 * 
	 * @param listener
	 *            the listener to add
	 */
	public void addEventListener(WiiRemoteEventListener listener) {
		listeners.add(listener);
	}

	/**
	 * Remove an event listener.
	 * 
	 * <p>
	 * Does nothing if listener not registered.
	 * 
	 * @param listener
	 *            the listener to remove
	 */
	public void removeEventListener(WiiRemoteEventListener listener) {
		listeners.add(listener);
	}

	public void setAccelerometerReporting(boolean accelerometerReporting) {
		remoteEndpoint
				.write(accelerometerReporting ? FULL_COMMAND_SET_REPORT_BUTTON_ACCELEROMETER
						: FULL_COMMAND_SET_REPORT_BUTTON);
	}

	/**
	 * Set a light on.
	 * 
	 * @param light
	 *            the light to turn on
	 */
	public void setLight(int light) {
		if (light >= 0 && light <= 3) {
			try {
				remoteEndpoint.write(FULL_COMMAND_LIGHTS[light]);
			} catch (Exception e) {
				throw new InteractiveSpacesException(
						"Error sending light command", e);
			}
		}
	}

	/**
	 * Read the calibration from the remote
	 */
	private void readCalibration() {
		try {
			remoteEndpoint.write(FULL_COMMAND_READ_CALIBRATION);
		} catch (Exception e) {
			throw new InteractiveSpacesException(
					"Error sending read calibration command", e);
		}
	}

	/**
	 * A button has been pressed. Notify all listeners.
	 * 
	 * @param button
	 *            the button which has been pressed
	 */
	private void notifyButtonEvent(int button) {
		for (WiiRemoteEventListener listener : listeners) {
			listener.onWiiRemoteButtonEvent(button);
		}
	}

	/**
	 * A button has been pressed. Notify all listeners.
	 * 
	 * @param button
	 *            the button which has been pressed
	 * @param x
	 *            the x component of the accelerometer
	 * @param y
	 *            the y component of the accelerometer
	 * @param z
	 *            the z component of the accelerometer
	 */
	private void notifyButtonAccelerometerEvent(int button, double x, double y,
			double z) {
		for (WiiRemoteEventListener listener : listeners) {
			listener.onWiiRemoteButtonAccelerometerEvent(button, x, y, z);
		}
	}

	/**
	 * The reader for information coming from the Wii
	 * 
	 * @author Keith M. Hughes
	 */
	private class RemoteReader implements Runnable {

		/**
		 * The read buffer
		 */
		private byte[] buffer = new byte[32];

		@Override
		public void run() {
			while (!Thread.interrupted()) {
				try {
					if (remoteEndpoint.read(buffer) == -1) {
						break;
					}

					switch (buffer[1]) {
					case REPORT_CALIBRATION_RESPONSE:
						decodeCalibrationResponse();

						break;

					case REPORT_BUTTON_ONLY:
						handleButtonOnlyEvent();

						break;

					case REPORT_BUTTON_ACCELEROMETER:
						handleButtonAccelerometerEvent();

						break;
					}
				} catch (Exception e) {
					log.error(String.format(
							"Error while reading Wii remote with address %s",
							address), e);

					// TODO(keith): Decide whether to look at message of string
					// to decide if to break or not. If remote turned off,
					// Bluecove gives "Peer closed connection".
					break;
				}
			}
		}

		/**
		 * Decode the calibration response.
		 */
		private void decodeCalibrationResponse() {
			byte b10 = buffer[10];
			calibrationX0 = ((buffer[7] & 0xFF) << 2) + (b10 & 3);
			calibrationY0 = ((buffer[8] & 0xFF) << 2) + ((b10 & 0xC) >> 2);
			calibrationZ0 = ((buffer[9] & 0xFF) << 2) + ((b10 & 0x30) >> 4);

			byte b14 = buffer[14];
			double calibrationX1 = ((buffer[11] & 0xFF) << 2) + (b14 & 3);
			double calibrationY1 = ((buffer[12] & 0xFF) << 2) + ((b14 & 0xC) >> 2);
			double calibrationZ1 = ((buffer[13] & 0xFF) << 2) + ((b14 & 0x30) >> 4);

			calibrationX = calibrationX1 - calibrationX0;
			calibrationY = calibrationY1 - calibrationY0;
			calibrationZ = calibrationZ1 - calibrationX0;
		}

		/**
		 * Handle a button only event.
		 */
		private void handleButtonOnlyEvent() {
			int button = ((buffer[2] << 8) & 0x1f00) | (buffer[3] & 0x9f);
			notifyButtonEvent(button);
		}

		/**
		 * Handle the full sensor event.
		 */
		private void handleButtonAccelerometerEvent() {
			byte b2 = buffer[2];
			byte b3 = buffer[3];
			int button = ((b2 << 8) & 0x1f00) | (b3 & 0x9f);

			int x = ((buffer[4] & 0xff) << 2) + ((b2 >> 5) & 0x03);
			
			// Byte 3 has bit 1 of y and z. No bit 0 for y and z
            int y = ((buffer[5] & 0xff) << 2) + ((b3 >> 4) & 0x02);
            int z = ((buffer[6] & 0xff) << 2) + ((b3 >> 5) & 0x02);

			double xaccel = ((double) x - calibrationX0) / (calibrationX);
			double yaccel = ((double) y - calibrationY0) / (calibrationY);
			double zaccel = ((double) z - calibrationZ0) / (calibrationZ);

			notifyButtonAccelerometerEvent(button, xaccel, yaccel, zaccel);
		}

	}
}
